package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;

import javax.annotation.Resource;
import java.awt.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    @Resource
    private CacheClient cacheClient;


    @Override
    public Result getShopById(Long id) {

        // 防止缓存穿透查询商户
        // Shop shop = getShopWithCacheThrough(id);
//        Shop shop = cacheClient.getWithCacheThrough(CACHE_SHOP_KEY, id, Shop.class,
//                this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);


        //互斥锁防止缓存击穿查询商户
//         = queryWithMutex(id);

        //逻辑过期防止缓存击穿查询用户
//        Shop shop = queryWithLogicalExprie(id);
        Shop shop = cacheClient.queryWithLogicalExprie(CACHE_SHOP_KEY, id, Shop.class, this::getById,
                CACHE_SHOP_TTL, TimeUnit.MINUTES);
        if (BeanUtil.isEmpty(shop)) {
            return Result.fail("该商户信息不存在！");
        }
        return Result.ok(shop);
    }


//    public Shop queryWithMutex(Long id) {
//        String key = CACHE_SHOP_KEY + id;
//        // 1、从redis中查询商铺缓存
//        String shopJson = stringRedisTemplate.opsForValue().get("key");
//        // 2、判断是否存在
//        if (StrUtil.isNotBlank(shopJson)) {
//            // 存在,直接返回
//            return JSONUtil.toBean(shopJson, Shop.class);
//        }
//        //判断命中的值是否是空值
//        if (shopJson != null) {
//            //返回一个错误信息
//            return null;
//        }
//        // 4.实现缓存重构
//
//        //4.1 获取互斥锁
//        String lockKey = "lock:shop:" + id;
//        Shop shop = null;
//        try {
//            boolean isLock = tryLock(lockKey);
//            // 4.2 判断否获取成功
//            if (!isLock) {
//                //4.3 失败，则休眠重试
//                Thread.sleep(50);
//                return queryWithMutex(id);
//            }
//            //4.4 成功，根据id查询数据库
//            shop = getById(id);
//            //模拟重建延迟
//            Thread.sleep(200);
//            // 5.不存在，返回错误
//            if (shop == null) {
//                //将空值写入redis
//                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//                //返回错误信息
//                return null;
//            }
//            //6.写入redis
//            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_NULL_TTL, TimeUnit.MINUTES);
//
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        } finally {
//            //7.释放互斥锁
//            (lockKey);
//        }
//        return shop;
//    }

    /**
     * 防止缓存穿透查询方法
     *
     * @param id
     * @return
     */
    public Shop getShopWithCacheThrough(Long id) {
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //a json转bean
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        //判断是否为空（“”），不为null为“”，返回空值
        if ("".equals(shopJson)) {
            return null;
        }

        //若没有数据则查询数据库
        Shop shop = getById(id);

        //判断数据库中是否有数据
        if (BeanUtil.isEmpty(shop)) {
            //数据库中没有数据时空值存入redis
            log.warn("没有该id的商家信息：{},向reids存入null", id);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, ""
                    , CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //数据库中有数据时存入redis并返回商家数据
        //将shop转为json字符串存入redis,设置有效期(有效期后添加随机值，防止缓存雪崩)
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop)
                , CACHE_SHOP_TTL + RandomUtil.randomLong(5L), TimeUnit.MINUTES);

        //返回商户信息
        return shop;
    }


    /**
     * 加载数据到redis
     */
    public void saveShopToRedis(Long id, Long expire) throws InterruptedException {
        //1.从数据库中得到商户信息
//        Shop shop = shopMapper.selectById(1L);
        Shop shop = getById(id);
        if (shop == null) {
            return;
        }
        //模拟延迟重建
        Thread.sleep(200);
        //2.封装成逻辑过期对象
        RedisData redisShop = RedisData.builder()
                .exprieTime(LocalDateTime.now().plusSeconds(expire))
                .data(shop).build();

        //3.存入redis
        //3.1对象转为json数据
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisShop));

    }

    /**
     * redis更新商户信息策略
     *
     * @param shop
     * @return
     */
    public Result updateByShop(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("商户id不能为空！");
        }

        // 1.更新数据库
        updateById(shop);

        //2.删除Redis缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);

        return Result.ok();
    }
}
